﻿using DotNetCommon;
using DotNetCommon.Extensions;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Test.SqlServer
{
    /// <summary>
    /// 测试Id和流水号生成,基于数据库缓存
    /// </summary>
    [TestFixture]
    public sealed partial class GeneratorTests : TestBase
    {
        private void ClearDBCache()
        {
            db.Manage.DropTableIfExist(db.Settings.DBCacheGeneratorIdTableName);
            db.Manage.DropTableIfExist(db.Settings.DBCacheGeneratorSNOTableName);
            db.Manage.DropTableIfExist(db.Settings.DBCacheGeneratorLogTableName);
            db.Manage.DropProcedureIfExist(db.Settings.DBCacheGeneratorIdProcedureName);
            db.Manage.DropProcedureIfExist(db.Settings.DBCacheGeneratorSNOProcedureName);
        }
        #region Id生成中的各种情况测试
        /// <summary>
        /// 当表不存在报错时
        /// </summary>
        [Test]
        public void TestNoTableError()
        {
            ClearDBCache();
            db.Manage.DropTableIfExist("test5");
            try
            {
                var id = db.NewId("test5", "id");
                throw new Exception("竟然没报错!");
            }
            catch (Exception ex)
            {
                int i = 0;
            }
        }

        /// <summary>
        /// 当表中还没有数据时
        /// </summary>
        [Test]
        public void TestTableHasNoData()
        {
            ClearDBCache();
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql("create table test(id int primary key,name varchar(50))");
            var id = db.NewId("test", "id");
            Assert.IsTrue(id == 1);
            id = db.NewId("test", "id");
            Assert.IsTrue(id == 2);
        }

        /// <summary>
        /// 当表中已有数据时
        /// </summary>
        [Test]
        public void TestTableHasData()
        {
            ClearDBCache();
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql("create table test(id int primary key,name varchar(50))");
            db.Insert("test", new { id = 1, name = "小明" }.ToDictionary());
            var id = db.NewId("test", "id");
            Assert.IsTrue(id == 2);
            id = db.NewId("test", "id");
            Assert.IsTrue(id == 3);
        }

        /// <summary>
        /// 当表中已有缓存时
        /// </summary>
        [Test]
        public void TestHasDataCache()
        {
            ClearDBCache();
            db.ExecuteSql(@"
CREATE TABLE [dbo].[__generator_id](
	[tablename] [varchar](50) NOT NULL,
	[colname] [varchar](50) NOT NULL,
	[currentid] [bigint] NULL,
PRIMARY KEY CLUSTERED 
(
	[tablename] ASC,
	[colname] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]");
            db.Insert("__generator_id", new { tablename = "test", colname = "id", currentid = 5 }.ToDictionary());
            var id = db.NewId("test", "id");
            Assert.IsTrue(id == 6);
            id = db.NewId("test", "id");
            Assert.IsTrue(id == 7);
        }

        /// <summary>
        /// 测试未获取到锁超时
        /// </summary>
        [Test]
        public void TestErrorTimeout()
        {
            ClearDBCache();
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql("create table test(id int primary key,name varchar(50))");
            var id = db.NewId("test", "id");
            Assert.IsTrue(id == 1);

            //制造一个锁 30秒
            var task1 = Task.Run(() =>
              {
                  try
                  {
                      var newDb = db.CreateNewDB();
                      newDb.RunInTransaction(() =>
                      {
                          newDb.ExecuteSql(@"
declare @result int=1;
EXEC @result =sp_getAppLock @resource='dbutil:newid:test_id',@lockMode='Exclusive',@lockOwner='Transaction',@lockTimeout='60000';
select @result");
                          Thread.Sleep(25 * 1000);
                      });
                  }
                  catch (Exception ex)
                  {
                      //不应该报错
                      throw ex;
                  }
              });
            var task2 = Task.Run(() =>
            {
                try
                {
                    Thread.Sleep(1000);
                    var id = db.NewId("test", "id");
                    throw new Exception("应该报超时的错!");
                }
                catch (Exception ex)
                {
                    //锁超时信息
                    Assert.IsTrue(ex.Message.Contains("20秒内"));
                }
            });
            Task.WaitAll(task1, task2);
        }
        #endregion

        #region Id生成
        [Test]
        public void NewId_DBCacheTest()
        {
            ClearDBCache();
            //准备测试表
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql(@"create table test(
id int primary key,
name varchar(50)
)");
            //单个id生成
            long id = db.NewId("test", "id");
            Assert.IsTrue(id == 1);
            id = db.NewId("test", "id");
            Assert.IsTrue(id == 2);
            //根据表中已有的id,继续生成
            db.ResetId("test", "id");
            var dic = new Dictionary<string, object>();
            dic.Add("id", 5);
            dic.Add("name", "xiaoming" + id);
            db.Insert("test", dic);
            id = db.NewId("test", "id");
            Assert.IsTrue(id == 6);
            //根据缓存批量生成id
            db.ResetId("test", "id");
            db.Manage.TruncateTable("test", "dbo");
            dic = new Dictionary<string, object>();
            dic.Add("id", 2);
            dic.Add("name", "xiaoming" + id);
            db.Insert("test", dic);
            id = db.NewId("test", "id");
            Assert.IsTrue(id == 3);
            var ids = db.NewIds("test", "id", 5);
            var expect = new List<long>() { 4, 5, 6, 7, 8 };
            var expectStr = string.Join(",", expect);
            var actStr = string.Join(",", ids);
            Assert.IsTrue(expectStr == actStr);
            //根据表中已有的id,继续批量生成
            db.ResetId("test", "id");
            ids = db.NewIds("test", "id", 5);
            expect = new List<long>() { 3, 4, 5, 6, 7 };
            expectStr = string.Join(",", expect);
            actStr = string.Join(",", ids);
            Assert.IsTrue(expectStr == actStr);
            //直接从缓存中生成
            db.Manage.TruncateTable("test", "dbo");
            db.ResetId("test", "id");
            ids = db.NewIds("test", "id", 5);
            expect = new List<long>() { 1, 2, 3, 4, 5 };
            expectStr = string.Join(",", expect);
            actStr = string.Join(",", ids);
            Assert.IsTrue(expectStr == actStr);
        }

        [Test]
        public void NewIdConcurrency_DBCacheTest()
        {
            ClearDBCache();
            //准备测试表
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql(@"create table test(
id int primary key,
name varchar(50)
)");
            var tasks = new List<Task>();
            var idres = new System.Collections.Concurrent.ConcurrentQueue<(long id, int threadid)>();
            for (int i = 0; i < 100; i++)
            {
                tasks.Add(Task.Factory.StartNew(() =>
                {
                    try
                    {
                        var db2 = db.CreateNewDB();
                        for (int j = 0; j < 100; j++)
                        {
                            var idg = db2.NewId("test", "id");
                            idres.Enqueue((idg, Thread.CurrentThread.ManagedThreadId));
                        }
                    }
                    catch (Exception ex)
                    {
                        Debug.Write(ex?.Message + ex?.StackTrace);
                        Debug.Fail(ex?.Message, ex?.StackTrace);
                    }
                }, TaskCreationOptions.LongRunning));
            }
            Task.WaitAll(tasks.ToArray());
            var list = idres.ToList();
            list = list.OrderByDescending(t => t.id).ToList();
            var expactCount = 100 * 100;
            Assert.IsTrue(list.LastOrDefault().id == 1);
            Assert.IsTrue(list.FirstOrDefault().id == expactCount);
            Assert.IsTrue(list.Count == expactCount);
        }
        #endregion

        #region SNO生成中的各种情况测试
        /// <summary>
        /// 表不存在,报错
        /// </summary>
        [Test]
        public void TestErrorSql()
        {
            ClearDBCache();
            db.Manage.DropTableIfExist("test");
            try
            {
                var sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
                throw new Exception("竟然没报错!");
            }
            catch (Exception ex)
            {
                int i = 0;
            }
        }

        /// <summary>
        /// 无缓存 表中没有匹配数据
        /// </summary>
        [Test]
        public void TestNoCacheNoData()
        {
            ClearDBCache();
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql("create table test(sno varchar(50))");
            var sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
            Assert.IsTrue(sno == $"SNO{DateTime.Now.ToString("yyyyMMdd")}000001");
            sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
            Assert.IsTrue(sno == $"SNO{DateTime.Now.ToString("yyyyMMdd")}000002");
        }

        /// <summary>
        /// 无缓存 表中有匹配数据
        /// </summary>
        [Test]
        public void TestNoCacheHasData()
        {
            ClearDBCache();
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql("create table test(sno varchar(50))");
            db.Insert("test", new { sno = $"SNO{DateTime.Now.ToString("yyyyMMdd")}000005" }.ToDictionary());
            var sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
            Assert.IsTrue(sno == $"SNO{DateTime.Now.ToString("yyyyMMdd")}000006");
            sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
            Assert.IsTrue(sno == $"SNO{DateTime.Now.ToString("yyyyMMdd")}000007");
        }

        /// <summary>
        /// 有缓存,时间戳对不上
        /// </summary>
        [Test]
        public void TestHasCacheWrongTimeStamp()
        {
            ClearDBCache();
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql(@"
create table __generator_sno
(
	tablename varchar(50),
	colname varchar(50),
	statictext varchar(200),
	machineidstr varchar(50),
	nowstr varchar(100),
	currentno bigint,
	primary key(tablename,colname,statictext)
)");
            db.Insert("__generator_sno", new { tablename = "test", colname = "sno", statictext = $"SNO", nowstr = DateTime.Now.AddDays(-1).ToCommonString(), currentno = "2" }.ToDictionary());
            var sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));

            Assert.IsTrue(sno == $"SNO{DateTime.Now.ToString("yyyyMMdd")}000001");
            sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
            Assert.IsTrue(sno == $"SNO{DateTime.Now.ToString("yyyyMMdd")}000002");
        }

        /// <summary>
        /// 有缓存,时间戳对得上
        /// </summary>
        [Test]
        public void TestHasCacheRightTimeStamp()
        {
            ClearDBCache();
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql(@"
create table __generator_sno
(
	tablename varchar(50),
	colname varchar(50),
	statictext varchar(200),
	machineidstr varchar(50),
	nowstr varchar(100),
	currentno bigint,
	primary key(tablename,colname,statictext)
)");
            db.Insert("__generator_sno", new { tablename = "test", colname = "sno", statictext = $"SNO", nowstr = DateTime.Now.ToString("yyyyMMdd"), currentno = "2" }.ToDictionary());
            var sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));

            Assert.IsTrue(sno == $"SNO{DateTime.Now.ToString("yyyyMMdd")}000003");
            sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
            Assert.IsTrue(sno == $"SNO{DateTime.Now.ToString("yyyyMMdd")}000004");
        }

        [Test]
        public void TestLockTimeout()
        {
            ClearDBCache();
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql("create table test(sno varchar(50))");
            var sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
            Assert.IsTrue(sno == $"SNO{DateTime.Now.ToString("yyyyMMdd")}000001");

            //制造一个锁 30秒
            var task1 = Task.Factory.StartNew(() =>
            {
                try
                {
                    var newDb = db.CreateNewDB();
                    newDb.RunInTransaction(() =>
                    {
                        newDb.ExecuteSql(@"
declare @result int=1;
EXEC @result =sp_getAppLock @resource='dbutil:newsno:test_sno_SNO',@lockMode='Exclusive',@lockOwner='Transaction',@lockTimeout='60000';
select @result");
                        Thread.Sleep(30 * 1000);
                    });
                }
                catch (Exception ex)
                {
                    //不应该报错
                    throw ex;
                }
            });
            var task2 = Task.Factory.StartNew(() =>
            {
                try
                {
                    Thread.Sleep(1000);
                    var id = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
                    throw new Exception("应该报超时的错!");
                }
                catch (Exception ex)
                {
                    //锁超时信息
                    Assert.IsTrue(ex.Message.Contains("20秒内"));
                }
            });
            Task.WaitAll(task1, task2);
        }
        #endregion

        #region SNO生成
        [Test]
        public void NewSNO_DBCacheTest()
        {
            ClearDBCache();
            //准备测试表
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql(@"create table test(
id int primary key,
sno varchar(50),
name varchar(50)
)");
            //单个生成
            var sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
            Assert.IsTrue(sno == "SNO" + DateTime.Now.ToString("yyyyMMdd") + "000001");
            sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
            Assert.IsTrue(sno == "SNO" + DateTime.Now.ToString("yyyyMMdd") + "000002");
            //根据表中已有的sno,继续生成
            var dic = new Dictionary<string, object>();
            var id = db.NewId("test", "id");
            dic.Add("id", id);
            dic.Add("name", "xiaoming" + id);
            dic.Add("sno", sno);
            db.Insert("test", dic);
            db.ResetSNO("test", "sno");
            sno = db.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
            Assert.IsTrue(sno == "SNO" + DateTime.Now.ToString("yyyyMMdd") + "000003");
            //根据缓存批量生成sno
            var snos = db.NewSNOs("test", "sno", SerialFormat.CreateFast("SNO"), 5);
            var expect = new List<string>() { $"SNO{DateTime.Now.ToString("yyyyMMdd")}000004", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000005", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000006", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000007", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000008" };
            var expectStr = string.Join(",", expect);
            var actStr = string.Join(",", snos);
            Assert.IsTrue(expectStr == actStr);
            //根据表中已有的id,继续批量生成
            db.ResetSNO("test", "sno");
            snos = db.NewSNOs("test", "sno", SerialFormat.CreateFast("SNO"), 5);
            expect = new List<string>() { $"SNO{DateTime.Now.ToString("yyyyMMdd")}000003", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000004", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000005", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000006", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000007" };
            expectStr = string.Join(",", expect);
            actStr = string.Join(",", snos);
            Assert.IsTrue(expectStr == actStr);
            //直接从缓存中生成
            db.Manage.TruncateTable("test", "dbo");
            db.ResetSNO("test", "sno");
            snos = db.NewSNOs("test", "sno", SerialFormat.CreateFast("SNO"), 5);
            expect = new List<string>() { $"SNO{DateTime.Now.ToString("yyyyMMdd")}000001", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000002", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000003", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000004", $"SNO{DateTime.Now.ToString("yyyyMMdd")}000005" };
            expectStr = string.Join(",", expect);
            actStr = string.Join(",", snos);
            Assert.IsTrue(expectStr == actStr);
        }


        [Test]
        public void NewSNO_DBCacheConcurrencyTest()
        {
            ClearDBCache();
            //准备测试表
            db.Manage.DropTableIfExist("test");
            db.ExecuteSql(@"create table test(
id int primary key,
sno varchar(50),
name varchar(50)
)");
            var tasks = new List<Task>();
            var snores = new System.Collections.Concurrent.ConcurrentQueue<(string sno, int threadid)>();
            for (int i = 0; i < 100; i++)
            {
                tasks.Add(Task.Factory.StartNew(() =>
                {
                    try
                    {
                        var db2 = db.CreateNewDB();
                        for (int j = 0; j < 100; j++)
                        {
                            var sno = db2.NewSNO("test", "sno", SerialFormat.CreateFast("SNO"));
                            snores.Enqueue((sno, Thread.CurrentThread.ManagedThreadId));
                        }
                    }
                    catch (Exception ex)
                    {
                        Debug.Write(ex?.Message + ex?.StackTrace);
                        Debug.Fail(ex?.Message, ex?.StackTrace);
                    }
                }, TaskCreationOptions.LongRunning));
            }
            Task.WaitAll(tasks.ToArray());
            var list = snores.ToList();
            list = list.OrderByDescending(t => t.sno).ToList();
            var expactCount = 100 * 100;
            Assert.IsTrue(list.FirstOrDefault().sno == $"SNO{DateTime.Now.ToString("yyyyMMdd")}{expactCount.ToString().PadLeft(6, '0')}");
            Assert.IsTrue(list.Count == expactCount);
        }
        #endregion
    }
}
